# -*- mode: python; coding: utf-8; -*-
##--------------------------------------------------------------------------##
##
## Copyright (C) 1998-2003 Markus Oberhumer <markus@oberhumer.com>
## Copyright (C) 2003 Mt. Hood Playing Card Co.
## Copyright (C) 2005-2010 Skomoroh <skomoroh@users.sourceforge.net>
## Copyright (C) 2010-2013 Jeremy Austin-Bardo <tjaustinbardo@gmail.com>
##
## This program is free software: you can redistribute it and/or modify
## it under the terms of the GNU General Public License as published by
## the Free Software Foundation, either version 3 of the License.
##
## This program is distributed in the hope that it will be useful,
## but WITHOUT ANY WARRANTY; without even the implied warranty of
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
## GNU General Public License for more details.
##
## You should have received a copy of the GNU General Public License
## along with this program.  If not, see <http://www.gnu.org/licenses/>.
##
##--------------------------------------------------------------------------##
"""
Methods for all toolkits
========================

File Menu:
  Load / dump game data. Start new game of current id.

Select Menu:
  Choose a new game from a chooser or menu options.

Edit Menu:
  Control play of game with bookmarks, undo and redo.
"""
__docformat__ = 'restructuredtext en'
n_ = lambda x: x
_ = lambda x: x

import functools
import os
import tkinter.filedialog

from .select import GameChooser as _GameChooser
from .widget import Menu
from .dialog import Dialog
from .constants import CURSOR_WATCH
from ..gamelib.data import GAME_DB
from ..gamelib import group as GI, state
from ..gamelib.info import GT
from ..pysolaudio import AUDIO_DEV
from ..statdb import STAT_DB
from ..settings import TITLE, CONF_OPT, SELECT_GAME_MENU
from ..utillib.pathdb import PATH_DB
from ..utillib.game import load as load_game, dump as dump_game


def split_initial_path(path):
    """
    split_inital_path(path) -> str

    Returns initial dir and file for a game data file.
    """
    if path:
        dir_, file_ = os.path.split(os.path.normpath(path))
    else:
        dir_, file_ = [''] * 2
    dir_ = dir_ or PATH_DB.dn['savegames']
    return dir_, file_


def confirm_(cls, text, restart=False):
    """
    confirm_(cls:Game, text:str, restart:bool=False) -> bool

    Get player confirmation for menu choice.
    """
    if cls.is_preview:
        return True
    if not cls.changed(restart=restart):
        return True
    return Dialog.confirm(text=text)


class FileMenuMixin(object):
    """
    Build the File game menu.
    """
    DEFAULTEXTENSION = '.pso'
    FILETYPES = ((_('{} files').format(TITLE), '*{}'.format(DEFAULTEXTENSION)),
             (_('All files'), '*'),)

    game = top = app = None

    def connect_game(self, game):
        """
        Connect file variables to Game object. Call next method in order.

        .. connect_game(self, game:Game) -> None
        """
        self._update_recent_menu(game['id'])
        self._update_favorite_menu()

        ancestor = super(FileMenuMixin, self)
        if hasattr(ancestor, 'connect_game'):
            ancestor.connect_game(game)

    def _add_file_menu(self, menu):
        """
        Create file menu.
          - [command] New game
          - [submenu] Recent games (updates after game connect)
          - [submenu] Select random game
          - [command] Select game by nu&mber...
          - [submenu] Favorite games (updates after game connect)
          - [command] Add to favorites
          - [command] Remove from favorites
          - [command] Open...
          - [command] Save
          - [command] Save as...
          - [command] Hold and quit
          - [command] Quit

        .. \_add_file_menu(menu:Menu) -> None
        """
        menu.add_command(label=n_('&New game'), accelerator='N',
                         command=self._new_game)
        Menu(menu, label=n_('R&ecent games'))  # updates after
        self._add_random_menu(Menu(menu, label=n_('Select &random game')))
        menu.add_command(label=n_('Select game by nu&mber...'),
                         accelerator=self._meta_mkey.format('M'),
                         command=self._choose_game_by_seed, )
        menu.add_separator()
        Menu(menu, label=n_('Fa&vorite games'))  # updates after
        menu.add_command(label=n_('A&dd to favorites'),
                         command=self._update_favorite_option)
        menu.add_command(label=n_('Remove &from favorites'),
                         command=self._update_favorite_option)
        menu.add_separator()
        menu.add_command(label=n_('&Open...'),
                         accelerator=self._meta_mkey.format('O'),
                         command=self._open_game_data)
        menu.add_command(label=n_('&Save'),
                         accelerator=self._meta_mkey.format('S'),
                         command=self._save_game_data)
        menu.add_command(label=n_('Save &as...'),
                         command=self._save_new_game_data)
        menu.add_separator()
        menu.add_command(label=n_('&Hold and quit'),
                         accelerator=self._meta_mkey.format('X'),
                         command=lambda h=True: self._quit_game(holdgame=h))
        menu.add_command(label=n_('&Quit'),
                         accelerator=self._meta_mkey.format('Q'),
                         command=self._quit_game)

    def _add_random_menu(self, menu):
        """
        Create random game menu.
          - [command] All games command
          - [command] Games played and won
          - [command] Games played and not won
          - [command] Games not played

        .. \_add_random_menu(menu:Menu) -> None
        """
        menu.add_command(label=n_('&All games'),
                         accelerator=self._meta_mkey.format('R'),
                         command=lambda : self._new_random_game('all'))
        menu.add_command(label=n_('Games played and &won'),
                         command=lambda : self._new_random_game('won'))
        menu.add_command(label=n_('Games played and &not won'),
                         command=lambda : self._new_random_game('not won'))
        menu.add_command(label=n_('Games not &played'),
                         command=lambda : self._new_random_game('not played'))

    def create_keybindings(self):
        """
        Bind menu command to key press combo event. Call next method in order.
          - <n> new game.
          - <*meta*-r> random game.
          - <*meta*-m> choose next game seed.
          - <*meta*-n> next game by seed.
          - <*meta*-o> open game.
          - <*meta*-s> save game.
          - <*meta*-x> hold game and quit app.
          - <*meta*-q> quit game and quit app.
          - <*meta*-PageUp> prior game by name
          - <*meta*-PageDown> next game by name
          - <*meta*-Up> prior game by id
          - <*meta*-Down> next game by id

        .. create_keybindings() -> None
        """
        meta = self._meta_bkey
        self._bind_keypress(None, 'n',
                            lambda e: self._new_game())
        self._bind_keypress(meta, 'r',
                            lambda e: self._new_random_game())
        self._bind_keypress(meta, 'm',
                            lambda e: self._choose_game_by_seed())
        self._bind_keypress(meta, 'n',
                            lambda e: self.__new_game_by_seed())
        self._bind_keypress(meta, 'o',
                            lambda e: self._open_game_data())
        self._bind_keypress(meta, 's',
                            lambda e: self._save_game_data())
        self._bind_keypress(meta, 'x',
                            lambda e, h=True: self._quit_game(holdgame=h))
        self._bind_keypress(meta, 'q',
                            lambda e: self._quit_game())
        self._bind_keypress(meta, 'Prior',
                      lambda e, n='name', s=-1: self._next_game_from_list(n, s))
        self._bind_keypress(meta, 'Next',
                      lambda e, n='name', s=+1: self._next_game_from_list(n, s))
        self._bind_keypress(meta, 'Up',
                      lambda e, n='id', s=-1: self._next_game_from_list(n, s))
        self._bind_keypress(meta, 'Down',
                      lambda e, n='id', s=+1: self._next_game_from_list(n, s))

        ancestor = super(FileMenuMixin, self)
        if hasattr(ancestor, 'create_keybindings'):
            ancestor.create_keybindings()

    def _update_favorite_option(self):
        """
        Update favorite game id config option. Then call update favorite_menu.
        Remove current game when present. Append current game when not present.

        .. \_update_favorite_option() -> None
        """
        gid = self.game['id']
        if gid not in CONF_OPT.get('game_fave', []):
            CONF_OPT['game_fave'].append(gid)
        elif gid in CONF_OPT.get('game_fave', []):
            CONF_OPT['game_fave'].remove(gid)
        self._update_favorite_menu()

    def _update_favorite_menu(self):
        """
        Update favorite games menu. Called by connect game and update favorite
        option. Games added by game's id from favorite game id config option.

        .. \_update_recent_menu() -> None
        """
        # Update submenu
        self._update_file_gamemenu([GAME_DB.find(i, 'id')
                                    for i in CONF_OPT.get('game_fave', [])
                                    if GAME_DB.find(i, 'id')], 'favorite')

        # Update command enabled
        in_favor = self.game['id'] in CONF_OPT.get('game_fave', [])
        self.set_state(not in_favor, 'file.addtofavorites')
        self.set_state(in_favor, 'file.removefromfavorites')

    def _update_recent_menu(self, gid):
        """
        Update recent game id config option. Then update recent game menu items.
        Called by connect game. Note gid given should be game's id not index.

        .. \_update_recent_menu(gid:int) -> None
        """
        #Update config
        recent_games = CONF_OPT.get('game_recent', [])
        if gid in recent_games:
            recent_games.remove(gid)
        recent_games.insert(0, gid)
        CONF_OPT['game_recent'] = recent_games[:CONF_OPT['game_recent_num']]

        # Update submenu
        games = [GAME_DB.find(gid, 'id')
                    for gid in CONF_OPT.get('game_recent', [])
                    if GAME_DB.find(gid, 'id')]
        self._update_file_gamemenu(games, 'recent')

    def _file_chooser(self, action, idir, ifile):
        """
        Return file path from file dialog query for given action. Values idir
        and ifile given set intial dir and file.

        .. \_file_chooser(action:str, idir:str, ifile:str) -> str
        """
        query_dialog = {'open': tkinter.filedialog.Open,
                        'save': tkinter.filedialog.SaveAs}.get(action)
        if query_dialog is None:
            return
        return query_dialog().show(filetypes=self.FILETYPES,
                                   defaultextension=self.DEFAULTEXTENSION,
                                   initialdir=idir, initialfile=ifile)

    def update_menu_state(self, clear=False):
        """
        Update file menu items enable state.

        .. update_menu_state(clear:bool=False) -> None
        """
        from .console import TOOL_BAR

        if clear:
            hold_and_quit = save_as = save = False
        else:
            hold_and_quit = save_as = self.game._can_save_
            save = bool(self.game.filename and save_as)

        self.set_state(save, 'file.save')
        self.set_state(save_as, 'file.saveas')
        self.set_state(hold_and_quit, 'file.holdandquit')

        TOOL_BAR.update_region_state(save=save_as)

        ancestor = super(FileMenuMixin, self)
        if hasattr(ancestor, 'update_menu_state'):
            ancestor.update_menu_state()

    @state.cancel_drag(True)
    @state.not_state('is_preview')
    def _new_game(self):
        """
        Start new game of same id.

        _new_game() -> None
        """
        game = self.game
        if confirm_(game, _("Start new game?")):
            if game.nextgame_flags(game['id']) == 0:  # NO_CHANGE
                game.new()
            else:
                game.quit(id_=game['id'])

    @state.cancel_drag(True)
    @state.not_state('is_preview')
    def _new_game_by_id(self, id_=None, random=None):
        """
        Start new game of another id.

        .. \_new_game_by_id([id_:int=get_option(gameid), random:Random=None])
             -> None
        """
        id_ = id_ or self._get_option('gameid')
        game = self.game

        flags = game.nextgame_flags(id_, random)
        # check NO_CHANGE or CARDSET_CHANGE
        if flags & 17 == 0 or not confirm_(game, _("Start game choice?")):
            return
        # check NO_CHANGE or SEED_CHANGE
        if flags & 15 == 0:
            game.new(random=random)
        else:
            game.quit(id_=id_, seed=random)

    @state.game_connect
    def __new_game_by_seed(self, seed=None):
        """
        Start new game of another seed.

        .. \__new_game_by_seed([seed:int=NextSeed]) -> None

        """
        from ..packlib.shuffle import random_class
        game = self.game
        random_class = random_class(seed or game.random._nextseed)
        if random_class:
            self._new_game_by_id(game['id'], random=random_class)

    @state.cancel_drag(False)
    def _choose_game_by_seed(self):
        """
        Choose new seed then start new game with the seed.

        .. \_choose_game_by_seed() -> None
        """
        select = Dialog.select(text=_("Enter new game number"),
                               value=self.game.random)
        if select is not None:
            self.__new_game_by_seed(select)

    @state.cancel_drag(False)
    def _new_random_game(self, type_='all'):
        """
        Start new random game of type that is not current game id.

        .. \_new_random_game(type_:str='all') -> None
        """
        game = self.game

        def selection_iter(cgi, what):
            """
            Iter game db sorted by name for what games based on stat db data.
            """
            for item in GAME_DB.sort('name'):
                sgi = GAME_DB[item]
                if any(sgi[k] == cgi[k] for k in ['id', 'category']):  # same
                    continue
                won, lost = STAT_DB.basic(CONF_OPT['player'], sgi['id'])
                if what == 'all':
                    yield sgi['id']
                elif what == 'won' and won > 0 and lost == 0:
                    yield sgi['id']
                elif what == 'not won' and won == 0 and lost > 0:
                    yield sgi['id']
                elif what == 'not played' and won + lost == 0:
                    yield sgi['id']

        games = [gid for gid in selection_iter(game, type_)]

        if games:
            from random import choice
            self._new_game_by_id(choice(games))
        else:
            Dialog.warning(text=_('No games of {} type exists.'.format(type_)))

    @state.cancel_drag(False)
    def _next_game_from_list(self, sort_name='all', step=1):
        """
        Start next game from list of sort name type. Game selected is offset by
        step from current game index.

        _next_game_from_list([sort_name:str='all', step:int=1]) -> None
        """
        games = GAME_DB.sort(sort_name)
        if len(games) < 2 or not self.game['id'] in games:
            return
        id_ = (games.index(GAME_DB.find(self.game['id'])) + step) % len(games)
        self._new_game_by_id(id_)

    @property
    def __filename(self):
        """
        Returns default filename for a game data file.

        __filename -> str
        """
        name = self.game['save_filename']
        return "{0}-{1.random}.{2}".format(name.lower(), game,
                                            self.DEFAULTEXTENSION)

    @state.cancel_drag(True)
    def _save_game_data(self, filename=None):
        """
        Save game to filename. When no filename call save new game data to get
        a new preferred filename for game data.

        .. \_save_game_data(filename:str=Game.filename) -> None
        """
        game = self.game
        assert game._can_save_
        filename = filename or game.filename
        if filename:
            self.update_menu_state(clear=True)
            game.filename = filename
            self.top.wm_set_cursor(cursor=CURSOR_WATCH)
            dump_game(game, filename=filename)
            self.top.wm_set_cursor()
            self.update_menu_state()
        else:
            self._save_new_game_data()

    @state.cancel_drag(True)
    def _save_new_game_data(self):
        """
        Set a suggested file name and path then call filechooser to get a new
        preferred file name and path for game data.

        .. \_save_new_game_data() -> None
        """
        game = self.game
        assert game._can_save_
        idir, ifile = split_initial_path(game.filename or self.__filename)
        filename = self._file_chooser('save', idir, ifile)
        if filename:
            self._save_game_data(filename)

    @state.cancel_drag(False)
    def _open_game_data(self):
        """
        Set a suggested file name and path then call filechooser to get a file
        name and path for game data to load.

        .. \_open_game_data() -> None
        """
        game = self.game
        idir, ifile = split_initial_path(game.filename)
        filename = self._file_chooser('open', idir, ifile)
        if filename and os.path.isfile(filename):
            self.update_menu_state(clear=True)
            self.top.wm_set_cursor(cursor=CURSOR_WATCH)
            next_game = load_game(filename=filename)
            if not next_game:
                Dialog.warning(text=_('Could not load game.'))
                return
            self.top.wm_set_cursor()
            self.update_menu_state()
            # now start the new game
            if game.next_gameflags(next_game['game_id']) == 0:
                game.restore(next_game, autoplay=False)
            else:
                game.quit(data=next_game, mode='load')

    @state.cancel_drag(True)
    def _quit_game(self, holdgame=False):
        """
        Quit application. Optionally hold game for next time.

        .. \_quit_game([holdgame:bool=False]) -> None
        """
        game = self.game
        hold = holdgame and self.game._can_save_
        if confirm_(game, text='{0} {1}'.format(_('Quit'), TITLE)):
            game.quit(mode='hold' if hold else None)


def _count_games(games, select_data):
    """
    Returns number of **games** found with **select_data** filter.

    .. \_count_games(games, select_data) -> int
    """
    return sum(len([i for i in games if f(GAME_DB[i])]) for l, f in select_data)


class SelectMenuMixin(object):
    """
    Build the select game menu.
    """
    game = top = app = None

    def _set_option(self, key, value):
        """
        _set_option(key:str, value:bool) -> None

        Stub set value of tkVariable in given key to given value.
        """
        raise NotImplementedError

    def connect_game(self, game):
        """
        connect_game(self, game:Game) -> None

        Connect select tkVariables to value in Game object. Then call parent
        method update other tkVariables.
        """
        self._set_option('gameid', game['id'])

        ancestor = super(SelectMenuMixin, self)
        if hasattr(ancestor, 'connect_game'):
            ancestor.connect_game(game)

    def _add_select_menu(self, menu):
        """"
        _add_select_menu(menu:Menu)

          - Playable preview... command
          - French games submenu
          - Mahjongg games submenu
          - Oriental games submenu
          - Special games submenu
          - All games by name submenu
        """
        menu.add_command(label=n_('Playable pre&view...'),
                         accelerator=self._meta_mkey.format('V'),
                         command=self._choose_game)
        #if not SELECT_GAME_MENU:
        #    return
        menu.add_separator()
        self.__add_game_submenu(menu, label=n_('&French games'),
                                select_data=GI.GAMES_BY_TYPE)
        self.__add_game_submenu_alpha(
                       Menu(menu, label=n_('&Mahjongg games')), shorten=True,
                       func=lambda gi: gi['si']['game_type'] == GT.MAHJONGG)
        self.__add_game_submenu(menu, label=n_('&Oriental games'),
                                select_data=GI.GAMES_ORIENT_BY_TYPE)
        self.__add_game_submenu(menu, label=n_('&Special games'),
                                select_data=GI.GAMES_OTHERS_BY_TYPE)
        menu.add_separator()
        self.__add_game_submenu_alpha(
                                  Menu(menu, label=n_('&All games by name')),
                                  func=lambda gid: True)

    def create_keybindings(self):
        """
        create_keybindings() -> None

        Bind seelect menu commands to different key press combination events.
        Then call parent method to bind additional events.
        """
        meta = self._meta_bkey
        self._bind_keypress(meta, 'v', lambda e: self._choose_game())

        ancestor = super(SelectMenuMixin, self)
        if hasattr(ancestor, 'create_keybindings'):
            ancestor.create_keybindings()

    def __add_game_submenu(self, menu, label, select_data, **kwargs):
        """
        __add_game_submenu(menu:Menu, label, select_data:seq(seq(str, func)...)
          [,shorten:bool=False]) -> None

        Append seq of sub menus from **select_data** on **menu**.
        """
        games = GAME_DB.sort('name')
        if _count_games(games, select_data) == 0:
            return

        if label:
            menu = Menu(menu, label)

        for label, func in select_data:
            sel_games = [i for i in games if func(GAME_DB[i])]
            if sel_games:
                self.__add_game_subsubmenu(sel_games, Menu(menu, label),
                                           **kwargs)

    def __add_game_submenu_alpha(self, menu, func, **kwargs):
        """
        __add_game_submenu_alpha(menu:Menu, func:func...)[, shorten:bool=False])
          -> None

        Append seq of sub menus of cb_max from **select_data** on **menu**. Each
        submenu will indicate the alpha range within.
        """
        key_name = 'short_name' if kwargs.get('shorten', False) else 'name'
        cb_ = self._cb_max
        games = GAME_DB.sort(key_name)

        def add_sub_menu(sel_games):
            """
            add_sub_menu(sel_games:seq(int ...)) -> None

            set label for submenu from first and last seq key_name value then
            create subsubmenu for seq.
            """
            chars = [GAME_DB[sel_games[i]][key_name][:3]
                     for i in [0, -1]]
            label = (chars[0] if chars[0] == chars[1]
                              else '{} - {}'.format(*chars))
            self.__add_game_subsubmenu(sel_games, Menu(menu, label, name=''),
                                       shorten=kwargs.get('shorten', False))

        col_games = []
        for gid in (i for i in games if func(GAME_DB[i])):
            col_games.append(gid)
            if len(col_games) >= cb_:
                add_sub_menu(col_games)
                col_games = []
        if len(col_games) > 0:
            add_sub_menu(col_games)

    def __add_game_subsubmenu(self, games, menu, shorten=False):
        """
        __add_game_subsubmenu(games:seq(int ...), menu:Menu[,
          shorten:bool=False]) -> None

        Append radio buttons to **menu**.
        """
        cb_ = self._cb_max
        key_name = 'short_name' if shorten else 'name'

        for index, gid in enumerate(games):
            menu.add_radiobutton(label=GAME_DB[gid][key_name],
                                 command=self._new_game_by_id,
                                 variable=self._get_variable('gameid'),
                                 columnbreak=index > 0 and (index % cb_) == 0,
                                 value=GAME_DB[gid]['id'])

    def _update_file_gamemenu(self, games, menu, shorten=False):
        """
        __update_file_gamemenu(games:seq(int ...), menu:label[,
          shorten:bool=False]) -> None

        Update radio buttons to **menu** under File.
        """
        menu = self._get_path('.menubar.file.{}games'.format(menu))[2]

        menu.delete(0, 'last')
        if len(games) == 0:
            menu.add_radiobutton(label='<none>', name=None, state='disabled')
        else:
            self.__add_game_subsubmenu(games, menu, shorten=shorten)

    @state.cancel_drag(False)
    def _choose_game(self):
        """
        _choose_game() -> None

        Call game chooser to select a game then start the new game if id change.
        """
        if _GameChooser is None:
            return
        game = self.game
        chooser = _GameChooser(self.top, title=_('Select game'), app=self.app,
                              choice=game['id'])

        if chooser.gameid and chooser.gameid != self.game['id']:
            self._new_game_by_id(chooser.gameid, random=chooser.random)


def check_bookmark(func):
    """
    @check_bookmark

    Use func only when bookmark config option and within range of 0-8.
    """

    @functools.wraps(func)
    def _check_bookmark(cls, *args, **kwargs):
        """
        Check id_ value or 2nd value is in range and config.
        """
        id_ = kwargs.get('id_', args[0])
        if CONF_OPT['bookmarks'] and (0 <= id_ <= 8):
            return func(cls, *args, **kwargs)

    return _check_bookmark


class EditMenuMixin(object):
    """
    Build the Edit menu.
    """
    game = top = app = None

    def set_state(self, *args):
        """
        set_state(state:bool, menu:Menu, index:int) -> None

        Subclass should set the menu item on path enable state.
        """
        raise NotImplementedError
    def _add_edit_menu(self, menu):
        """
        _add_edit_menu(menu:Menu) -> None

          - Undo command
          - Redo command
          - Redo all command
          - Set bookmark
            Bookmark 1 - 9
          - Goto bookmark
            Bookmark 1 - 9
          - Clear bookmarks
          - Restart
        """
        menu.add_command(label=n_('&Undo'),
                         accelerator='Z',
                         command=self._undo_game_move)
        menu.add_command(label=n_('&Redo'),
                         accelerator='R',
                         command=self._redo_game_move)
        menu.add_command(label=n_('Redo &all'),
                         command=self._redo_all_game_moves)
        menu.add_separator()
        set_menu = Menu(menu, label=n_('&Set bookmark'))
        get_menu = Menu(menu, label=n_('Go&to bookmark'))
        for mark in range(1, 10):
            set_menu.add_command(label=_('Bookmark {}').format(mark),
                               command=lambda m=mark: self._set_bookmark(m))
            get_menu.add_command(label=_('Bookmark {}').format(mark),
                               accelerator=self._meta_mkey.format(mark),
                               command=lambda m=mark, c=True:
                                            self._get_bookmark(m, confirm=c))
        menu.add_command(label=n_('&Clear bookmarks'),
                         command=self._clear_all_bookmarks)
        menu.add_separator()
        menu.add_command(label=n_('Restart'),
                         accelerator=self._meta_mkey.format('G'),
                         command=self._restart_game)

    def create_keybindings(self):
        """
        create_keybindings() -> None

        Bind edit menu commands to different key press combination events. Then
        call parent method to bind additional events.
        """
        meta = self._meta_bkey
        self._bind_keypress(None, 'z', lambda e: self._undo_game_move())
        self._bind_keypress(None, 's', lambda e: self._undo_game_move())
        self._bind_keypress(None, 'k', lambda e: self._undo_game_move())
        self._bind_keypress(None, 'r', lambda e: self._redo_game_move())
        self._bind_keypress(meta, 'g', lambda e: self._restart_game())

        for mark in range(1, 10):
            self._bind_keypress(meta, str(mark),
                          lambda e, m=mark, c=False: self._get_bookmark(m, c))

        # undocumented
        self._bind_keypress(None, 'BackSpace',
                            lambda e: self._undo_game_move())
        self._bind_keypress(None, 'KP_Enter',
                            lambda e: self._undo_game_move())

        ancestor = super(EditMenuMixin, self)
        if hasattr(ancestor, 'create_keybindings'):
            ancestor.create_keybindings()

    @state.game_connect
    def _update_bookmark_menu_state(self):
        """
        update_bookmark_menu_state() -> None

        Update bookmark menu and its menu items enable state.
        """
        set_mark_menu = self._get_path('.menubar.edit.setbookmark')[2]
        get_mark_menu = self._get_path('.menubar.edit.gotobookmark')[2]
        if set_mark_menu is None or get_mark_menu is None:
            return

        mark = bool(CONF_OPT['bookmarks'] and self.game._can_save_)

        _state = self._get_state

        mark_set = False
        for i in range(1, 10):
            # update set bookmark submenu
            set_mark_menu.entryconfig(i, state=_state(mark))

            # update goto bookmark submenu
            has_set = self.app.nextgame['bookmark'].get(i) is not None
            get_mark_menu.entryconfig(i - 1, state=_state(has_set and mark))
            mark_set = mark_set or has_set

        # update menus
        self.set_state(mark, 'edit.setbookmark')
        self.set_state(mark_set and mark, 'edit.gotobookmark')
        self.set_state(mark_set and mark, 'edit.clearbookmarks')

    @state.game_connect
    def update_menu_state(self, clear=False):
        """
        update_menu_state(clear:bool=False) -> None

        Update menu item enable state.
        """
        from .console import TOOL_BAR

        if clear:
            undo = redo = restart = False
        else:
            undo = self.game.can_undo_move
            restart = bool(self.game.move_index > 0)
            redo = undo and restart

        self.set_state(undo, 'edit.undo')
        self.set_state(redo, 'edit.redo')
        self.set_state(redo, 'edit.redoall')
        self._update_bookmark_menu_state()
        self.set_state(restart, 'edit.restart')

        TOOL_BAR.update_region_state(restart=restart, undo=undo, redo=redo)

        ancestor = super(EditMenuMixin, self)
        if hasattr(ancestor, 'update_menu_state'):
            ancestor.update_menu_state()

    @state.cancel_drag(True)
    def _undo_game_move(self):
        """
        _undo_game_move() -> None

        Undo an atomic game move
        """
        assert self.game.can_undo_move
        AUDIO_DEV.play('undo')
        self.game.undo()

    @state.cancel_drag(True)
    def _redo_game_move(self):
        """
        _undo_game_move() -> None

        redo an atomic game move
        """
        assert self.game.can_undo_move
        AUDIO_DEV.play('redo')
        self.game.redo()

    @state.cancel_drag(True)
    def _redo_all_game_moves(self):
        """
        _undo_game_move() -> None

        redo all atomic game moves.
        """
        assert self.game.can_undo_move
        self.top.update()
        self.game.redo_all()

    @check_bookmark
    @state.cancel_drag(True)
    def _set_bookmark(self, id_):
        """
        _set_bookmark(id_:int) -> None

        Set game bookmark for current point in game.
        """
        if not Dialog.confirm(text=_('Save bookmark?')):
            return
        game = self.game
        self.top.wm_set_cursor(cursor=CURSOR_WATCH)
        dump_game(game, bookmark=id_, update_stats=2)
        self.top.wm_set_cursor()
        self._update_bookmark_menu_state()

    @check_bookmark
    @state.cancel_drag(True)
    def _get_bookmark(self, id_, confirm=True):
        """
        _get_bookmark(id_:int, confirm:bool=True) -> None

        Get game bookmark from prior set bookmark points in game.
        """
        game = self.game
        bookmarks = self.app.nextgame['bookmark']

        if (not bookmarks.get(id_) or
            (confirm and not confirm_(game, text=_('Load bookmark?')))):
            return
        self.app.top.wm_set_cursor(cursor=CURSOR_WATCH)
        game_mark = load_game(bookmark=bookmarks[id_])

        self.app.top.wm_set_cursor()
        if not game_mark:
            del bookmarks[id_]
            Dialog.warning(text=_('Could not restore bookmark.'))
            return

        self.app.nextgame['counter']['bookmark'] += 1
        game.restore(game_mark, reset=False)
        del game_mark
        self.update_menu_state()

    @state.cancel_drag(True)
    def _clear_all_bookmarks(self):
        """
        _get_bookmark() -> None

        Clear all game bookmark points in game.
        """
        game = self.game
        if not any(CONF_OPT['bookmarks'], self.app.nextgame['bookmark']):
            return
        if not Dialog.confirm(text=_('Clear bookmarks?')):
            return
        self.app.nextgame['bookmark'] = {0: None}
        self._update_bookmark_menu_state()

    @state.cancel_drag(True)
    def _restart_game(self):
        """
        _restart_game() -> None

        Get game bookmark from prior set bookmark points in game.
        """
        game = self.game
        if game.move_index == 0:
            return
        if confirm_(game, text=_('Restart game?'), restart=True):
            game.new(restart=True)